namespace MicroFrameworkDriverLibrary.Devices
{
    using System;
    using Microsoft.SPOT.Hardware;

    /// <summary>
    /// DS1307 (Sparkfun Breakout Board) - RTC
    /// 
    /// Use this breakout board from 5V.
    /// </summary>
    public sealed class DS1307 : I2CDeviceHelper
    {
        // Real time clock I2C address
        private const ushort HARDWARE_BASE_ADDRESS = 0x68;

        // Total size of the user RAM block
        private const byte DS1307_RAM_SIZE = 56;

        // Start / End addresses of the user RAM registers
        private const byte DS1307_RAM_START_ADDRESS = 0x08;
        private const byte DS1307_RAM_END_ADDRESS = 0x3f;

        // Start / End addresses of the date/time registers
        private const byte DS1307_RTC_START_ADDRESS = 0x00;
        private const byte DS1307_RTC_END_ADDRESS = 0x06;

        // Square wave frequency generator register address
        private const byte DS1307_SQUARE_WAVE_CTRL_REGISTER_ADDRESS = 0x07;

        // Defines the frequency of the signal on the SQW interrupt pin on the clock when enabled
        public enum SquareWaveFrequency { SQW_1Hz, SQW_4kHz, SQW_8kHz, SQW_32kHz, SQW_Off };

        // Defines the logic level on the SQW pin when the frequency is disabled
        public enum SquareWaveDisabledOutputControl { Zero, One };

        public DS1307(I2CDevice sharedI2C, int clockRateKHz = 100, int transacitonTimeoutMs = 20, ushort hardwareAddress = HARDWARE_BASE_ADDRESS)
            : base(sharedI2C, hardwareAddress, clockRateKHz, transacitonTimeoutMs) { }

        protected override void Dispose(bool disposing)
        {
            base._disposed = true;
        }

        public byte[] NonVolatileRam
        {
            get
            {
                byte[] result = new byte[0];
                this.SwapConfiguration(() =>
                {
                    result = new byte[DS1307_RAM_SIZE];

                    var transaction = new I2CDevice.I2CTransaction[] {
            I2CDevice.CreateWriteTransaction(new byte[] { DS1307_RAM_START_ADDRESS }),
            I2CDevice.CreateReadTransaction(result) 
          };

                    if (this._sharedI2C.Execute(transaction, this._transactionTimeoutMs) == 0)
                    {
                        throw new Exception("I2C transaction failed");
                    }
                });
                return result;
            }
            set
            {
                this.SwapConfiguration(() =>
                {
                    if ((value == null) || (value.Length != DS1307_RAM_SIZE))
                    {
                        throw new ArgumentOutOfRangeException("Invalid buffer length");
                    }

                    // Allocate a new buffer large enough to include the RAM start address byte and the payload
                    var transactionBuffer = new byte[sizeof(byte) /*Address byte*/ + DS1307_RAM_SIZE];

                    // Set the RAM start address
                    transactionBuffer[0] = DS1307_RAM_START_ADDRESS;

                    // Copy the user buffer after the address
                    value.CopyTo(transactionBuffer, 1);

                    // Write to the clock's RAM
                    var transaction = new I2CDevice.I2CWriteTransaction[] { I2CDevice.CreateWriteTransaction(transactionBuffer) };

                    if (this._sharedI2C.Execute(transaction, this._transactionTimeoutMs) == 0)
                    {
                        throw new Exception("I2C write transaction failed");
                    }
                });
            }
        }

        public DateTime DateTime
        {
            get
            {
                // If we had true generic Func<T> and Action<T> delegates we would not need to do this local result.
                DateTime result = DateTime.MinValue;

                this.SwapConfiguration(() =>
                {
                    byte[] clockData = new byte[7];

                    // Read time registers (7 bytes from DS1307_RTC_START_ADDRESS)
                    var transaction = new I2CDevice.I2CTransaction[] {
            I2CDevice.CreateWriteTransaction(new byte[] { DS1307_RTC_START_ADDRESS }),
            I2CDevice.CreateReadTransaction(clockData)
          };

                    if (this._sharedI2C.Execute(transaction, this._transactionTimeoutMs) == 0)
                    {
                        throw new Exception("I2C transaction failed");
                    }

                    result = new DateTime(
                      BcdToDec(clockData[6]) + 2000, // year
                      BcdToDec(clockData[5]), // month
                      BcdToDec(clockData[4]), // day
                      BcdToDec(clockData[2] & 0x3f), // hours over 24 hours
                      BcdToDec(clockData[1]), // minutes
                      BcdToDec(clockData[0] & 0x7f) // seconds
                    );
                });
                return result;
            }
            set
            {
                this.SwapConfiguration(() =>
                {
                    var transaction = new I2CDevice.I2CWriteTransaction[] {
            I2CDevice.CreateWriteTransaction(new byte[] { 
              DS1307_RTC_START_ADDRESS, 
              DecToBcd(value.Second), 
              DecToBcd(value.Minute), 
              DecToBcd(value.Hour), 
              DecToBcd((int)value.DayOfWeek), 
              DecToBcd(value.Day), 
              DecToBcd(value.Month), 
              DecToBcd(value.Year - 2000)} )
          };

                    if (this._sharedI2C.Execute(transaction, this._transactionTimeoutMs) == 0)
                    {
                        throw new Exception("I2C write transaction failed");
                    }
                });
            }
        }

        public void SetSquareWave(SquareWaveFrequency squareWaveFrequency, SquareWaveDisabledOutputControl outputControl = SquareWaveDisabledOutputControl.Zero)
        {
            byte register = (byte)outputControl;

            register <<= 3;   // bit 7 defines the square wave output level when disabled
            // bit 6 & 5 are unused

            if (squareWaveFrequency != SquareWaveFrequency.SQW_Off)
            {
                register |= 1;
            }

            register <<= 4; // bit 4 defines if the oscillator generating the square wave frequency is on or off.
            // bit 3 & 2 are unused

            register |= (byte)squareWaveFrequency; // bit 1 & 0 define the frequency of the square wave

            this.SwapConfiguration(() =>
            {
                var transaction = new I2CDevice.I2CWriteTransaction[] {
                  I2CDevice.CreateWriteTransaction(new byte[] { DS1307_SQUARE_WAVE_CTRL_REGISTER_ADDRESS, register })
                };

                if (this._sharedI2C.Execute(transaction, this._transactionTimeoutMs) == 0)
                {
                    throw new Exception("I2C write transaction failed");
                }
            });
        }

        public static DateTime GetLocalTimeFromRealTimeClock(I2CDevice sharedI2C)
        {
            using (var realTimeClock = new DS1307(sharedI2C, clockRateKHz: 100))
            {
                // Get the userdata from the RTC storage (56 byts of data)
                byte[] realTimeClockUserData = realTimeClock.NonVolatileRam;

                // Get the timezone information from the first 2 bytes (number of minutes to offset)
                var timeZoneMinuteOffset = Utility.ExtractValueFromArray(realTimeClockUserData, 0, 2);

                // Get the time that is stored in the clock
                DateTime hardwareDateTime = realTimeClock.DateTime;

                // The time stored in the RTC is in GMT, so once we have it, we need to add our offset.
                return hardwareDateTime + new TimeSpan(TimeSpan.TicksPerMinute * (short)timeZoneMinuteOffset);
            }
        }
        public static TimeSpan GetLocalTimeZoneOffset(I2CDevice sharedI2C)
        {
            using (var realTimeClock = new DS1307(sharedI2C))
            {
                // Get the userdata from the RTC storage (56 byts of data)
                byte[] realTimeClockUserData = realTimeClock.NonVolatileRam;

                // Get the timezone information (number of minutes to offset)
                var timeZoneMinuteOffset = Utility.ExtractValueFromArray(realTimeClockUserData, 0, 2);

                // return the new TimeSpan
                return new TimeSpan(TimeSpan.TicksPerMinute * (short)timeZoneMinuteOffset);
            }
        }

        public static void SetRealTimeClock(I2CDevice sharedI2C, DateTime utcDateTime)
        {
            using (var realTimeClock = new DS1307(sharedI2C))
            {
                realTimeClock.DateTime = utcDateTime;
            }
        }
        public static void SetLocalTimeZoneOffset(I2CDevice sharedI2C, TimeSpan timeZoneOffset)
        {
            using (var realTimeClock = new DS1307(sharedI2C))
            {
                // Get the userdata from the RTC storage (56 byts of data)
                byte[] realTimeClockUserData = realTimeClock.NonVolatileRam;

                // Set the timezone information (number of minutes to offset)
                Utility.InsertValueIntoArray(realTimeClockUserData, 0, 2, (uint)(timeZoneOffset.Ticks / TimeSpan.TicksPerMinute));

                // Update the stored data
                realTimeClock.NonVolatileRam = realTimeClockUserData;
            }
        }
    }
}
